「スクリプトからシートを読むのは通るのに、行を追記しようとすると 403 で落ちる」「キーもAPIの有効化も合っているはずなのに、書き込みだけ権限がないと言われる」「権限を直したつもりが、まだ同じエラーが出る」
サービスアカウントで Google Sheets API を使うとき、こうした「読めるのに書けない」詰まりはよく起きます。原因の多くは、シートの共有設定でサービスアカウントが「閲覧者」のままになっていることです。ただし、同じ 403 でも原因は他に2つあり、直す場所が違います。
この記事では、読み取りと書き込みの両方を試す確認コード、403 の原因の切り分け表、直し方、書き込み前に取っておくバックアップの手順が分かります。
- 読み取りが通って書き込みだけ 403 なら、サービスアカウントの共有権限が「閲覧者」のことが多い
- 直し方は、シートの共有設定でサービスアカウントのメールアドレスを「編集者」にする
- 403 の文面が違えば別原因。API 未有効化とスコープ不足も同じ 403 で返る
- 書き込む前に、対象範囲の値をファイルへ保存してから追記する
目次を開く
症状:読み取りは通るのに書き込みだけ 403
典型的なエラーは、追記(values.append)や更新(values.update)で次のように返る形です。
HttpError 403 ... "The caller does not have permission"
status: PERMISSION_DENIED 同じスクリプトで values.get は成功します。読めているので、認証そのものは合っています。つまり「誰か」は通っていて、「その人に書く権限があるか」で止まっている状態です。
まず読み取りと書き込みを別々に試す
原因を推測する前に、読み取り・書き込みの結果を並べて見ます。次のコードは、対象シートの末尾セルへ1回だけ書いて、すぐ消す確認用です。
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
KEY_FILE = "service-account.json"
SHEET_ID = "<スプレッドシートID>"
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
creds = service_account.Credentials.from_service_account_file(KEY_FILE, scopes=SCOPES)
print("使っているアカウント:", creds.service_account_email)
sheets = build("sheets", "v4", credentials=creds).spreadsheets()
def try_call(label, fn):
try:
fn()
print(label, "OK")
except HttpError as e:
print(label, "NG", e.status_code, e.reason)
try_call("読み取り", lambda: sheets.values().get(spreadsheetId=SHEET_ID, range="A1:A1").execute())
try_call("書き込み", lambda: sheets.values().update(
spreadsheetId=SHEET_ID, range="Z1", valueInputOption="RAW",
body={"values": [["権限テスト"]]}).execute()) 書き込みに成功した場合は、テスト用のセル Z1 を手で消してください。出力の読み方は次のとおりです。
| 読み取り | 書き込み | 疑うもの |
|---|---|---|
| OK | NG(The caller does not have permission) | 共有権限が閲覧者 |
| NG | NG(同じ文面) | そもそもシートが共有されていない、またはID違い |
| NG | NG(insufficient authentication scopes) | スコープが読み取り専用 |
| NG | NG(API has not been used ... or it is disabled) | Sheets API が未有効 |
直し方:サービスアカウントを「編集者」で共有する
1. 共有すべきメールアドレスを確認する
上のコードが表示した creds.service_account_email が、共有先のアドレスです。キー JSON の中の client_email と同じ値です。
python3 -c "import json; print(json.load(open('service-account.json'))['client_email'])" 2. シートの共有設定で権限を「編集者」にする
スプレッドシートの右上「共有」を開き、上のアドレスを追加して権限を「編集者」にします。すでに追加済みなら、権限のドロップダウンを「閲覧者」から「編集者」に変えます。自分に共有権限がない場合は、シートのオーナーに依頼します。
3. もう一度確認コードを流す
書き込みが OK になれば完了です。共有の反映は通常すぐですが、変わらなければ数分待ってからやり直します。
スコープと API 有効化は別の直し方
共有権限が編集者なのに 403 が続くなら、切り分け表の他の行を確認します。
- スコープ不足:
SCOPESがspreadsheets.readonlyになっていないか確認する。書き込みにはhttps://www.googleapis.com/auth/spreadsheetsが必要 - API 未有効:クラウドコンソールで、サービスアカウントのプロジェクトの Google Sheets API を有効化する
書き込む前にスナップショットを保存する
権限が通った直後の最初の書き込みは、範囲や列の指定ミスで既存の値を壊しやすいタイミングです。書く前に、対象範囲の値を JSON で保存しておきます。
import json, datetime
RANGE = "Sheet1!A1:F1000"
before = sheets.values().get(spreadsheetId=SHEET_ID, range=RANGE).execute()
name = f"snapshot-{datetime.datetime.now():%Y%m%d-%H%M%S}.json"
with open(name, "w", encoding="utf-8") as f:
json.dump(before, f, ensure_ascii=False, indent=2)
print("保存:", name) シート名は自分の環境のものに置き換えてください。権限がまだ直っていない間は、追記する行を画面に出して手で貼り付ける運用にしておくと、作業を止めずに済みます。
まとめ
- 読み取りが通って書き込みだけ 403 なら、サービスアカウントの共有権限が閲覧者のことが多い
- 直し方は、
client_emailのアドレスをシートの共有設定で「編集者」にする - 同じ 403 でも、スコープ不足と API 未有効は文面が違う。切り分け表で確認する
- 書き込む前に、対象範囲をスナップショットとして保存する
よくある質問(FAQ)
編集者にしたのに、まだ 403 が出ます
スコープが spreadsheets.readonly のままになっていないか確認してください。エラー文に「insufficient authentication scopes」が含まれていれば、共有ではなくコード側の設定です。
サービスアカウントを自分のアカウントの共有先に追加できません
共有先の欄に client_email のアドレスをそのまま入力します。組織の共有制限でドメイン外への共有が禁止されている場合は、管理者に許可を依頼します。
確認コードで書いたセルは消したほうがよいですか
はい。確認用の Z1 は残さず、手で消してください。
読み取り専用で十分な用途でも編集者にすべきですか
いいえ。読むだけなら閲覧者のままで足ります。書き込みが必要なシートだけ編集者にすると、誤書き込みの範囲を絞れます。
