Repository: pandas-dev/pandas

Issue:
ENH: add `engine_kwargs` to DataFrame.to_excel
### Feature Type

- [X] Adding new functionality to pandas

- [ ] Changing existing functionality in pandas

- [ ] Removing existing functionality in pandas


### Problem Description

Currently the `DataFrame.to_excel` method does not allow users to pass keyword arguments directly to the Excel engine that is performing read / write operations on the users behalf. 

Passing keyword arguments to excel engines can be helpful for many reasons, as seen in issue  #40274

### Feature Description

The implementation would follow the same logic seen in PR #52214:

- Add `engine_kwargs` to the method to_excel
- engine_kwargs would be passed down to the ExcelWriter class and to all Excel engine _write_cells methods

### Alternative Solutions

N/A

### Additional Context

_No response_


Discussion:
take
@rhendric 

Created this enhancement request as discussed [here](https://github.com/pandas-dev/pandas/pull/52214#issuecomment-1491227994)
I'm not the user you meant to ping.
@rhshadrach 


Patch:
diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst
index 91cd7315f7213..90a8bd868b60b 100644
--- a/doc/source/user_guide/io.rst
+++ b/doc/source/user_guide/io.rst
@@ -3785,6 +3785,15 @@ one can pass an :class:`~pandas.io.excel.ExcelWriter`.
 
 .. _io.excel_writing_buffer:
 
+When using the ``engine_kwargs`` parameter, pandas will pass these arguments to the
+engine. For this, it is important to know which function pandas is using internally.
+
+* For the engine openpyxl, pandas is using :func:`openpyxl.Workbook` to create a new sheet and :func:`openpyxl.load_workbook` to append data to an existing sheet. The openpyxl engine writes to (``.xlsx``) and (``.xlsm``) files.
+
+* For the engine xlsxwriter, pandas is using :func:`xlsxwriter.Workbook` to write to (``.xlsx``) files.
+
+* For the engine odf, pandas is using :func:`odf.opendocument.OpenDocumentSpreadsheet` to write to (``.ods``) files.
+
 Writing Excel files to memory
 +++++++++++++++++++++++++++++
 
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 52fc8512c9db3..316934c44f3ed 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -97,8 +97,10 @@ Other enhancements
 - Let :meth:`DataFrame.to_feather` accept a non-default :class:`Index` and non-string column names (:issue:`51787`)
 - Performance improvement in :func:`read_csv` (:issue:`52632`) with ``engine="c"``
 - :meth:`Categorical.from_codes` has gotten a ``validate`` parameter (:issue:`50975`)
+- Added ``engine_kwargs`` parameter to :meth:`DataFrame.to_excel` (:issue:`53220`)
 - Performance improvement in :func:`concat` with homogeneous ``np.float64`` or ``np.float32`` dtypes (:issue:`52685`)
 - Performance improvement in :meth:`DataFrame.filter` when ``items`` is given (:issue:`52941`)
+-
 
 .. ---------------------------------------------------------------------------
 .. _whatsnew_210.notable_bug_fixes:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 93fecc4a7b096..4128b6e3a24c2 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -2155,6 +2155,7 @@ def to_excel(
         inf_rep: str = "inf",
         freeze_panes: tuple[int, int] | None = None,
         storage_options: StorageOptions = None,
+        engine_kwargs: dict[str, Any] | None = None,
     ) -> None:
         """
         Write {klass} to an Excel sheet.
@@ -2211,6 +2212,8 @@ def to_excel(
         {storage_options}
 
             .. versionadded:: {storage_options_versionadded}
+        engine_kwargs : dict, optional
+            Arbitrary keyword arguments passed to excel engine.
 
         See Also
         --------
@@ -2263,6 +2266,8 @@ def to_excel(
 
         >>> df1.to_excel('output1.xlsx', engine='xlsxwriter')  # doctest: +SKIP
         """
+        if engine_kwargs is None:
+            engine_kwargs = {}
 
         df = self if isinstance(self, ABCDataFrame) else self.to_frame()
 
@@ -2287,6 +2292,7 @@ def to_excel(
             freeze_panes=freeze_panes,
             engine=engine,
             storage_options=storage_options,
+            engine_kwargs=engine_kwargs,
         )
 
     @final
diff --git a/pandas/io/excel/_xlsxwriter.py b/pandas/io/excel/_xlsxwriter.py
index 8cb88403b2f87..d7262c2f62d94 100644
--- a/pandas/io/excel/_xlsxwriter.py
+++ b/pandas/io/excel/_xlsxwriter.py
@@ -212,7 +212,11 @@ def __init__(
             engine_kwargs=engine_kwargs,
         )
 
-        self._book = Workbook(self._handles.handle, **engine_kwargs)
+        try:
+            self._book = Workbook(self._handles.handle, **engine_kwargs)
+        except TypeError:
+            self._handles.handle.close()
+            raise
 
     @property
     def book(self):
diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py
index 457bbced87d55..41e5c22447833 100644
--- a/pandas/io/formats/excel.py
+++ b/pandas/io/formats/excel.py
@@ -901,6 +901,7 @@ def write(
         freeze_panes: tuple[int, int] | None = None,
         engine: str | None = None,
         storage_options: StorageOptions = None,
+        engine_kwargs: dict | None = None,
     ) -> None:
         """
         writer : path-like, file-like, or ExcelWriter object
@@ -922,6 +923,8 @@ def write(
         {storage_options}
 
             .. versionadded:: 1.2.0
+        engine_kwargs: dict, optional
+            Arbitrary keyword arguments passed to excel engine.
         """
         from pandas.io.excel import ExcelWriter
 
@@ -932,6 +935,9 @@ def write(
                 f"Max sheet size is: {self.max_rows}, {self.max_cols}"
             )
 
+        if engine_kwargs is None:
+            engine_kwargs = {}
+
         formatted_cells = self.get_formatted_cells()
         if isinstance(writer, ExcelWriter):
             need_save = False
@@ -939,7 +945,10 @@ def write(
             # error: Cannot instantiate abstract class 'ExcelWriter' with abstract
             # attributes 'engine', 'save', 'supported_extensions' and 'write_cells'
             writer = ExcelWriter(  # type: ignore[abstract]
-                writer, engine=engine, storage_options=storage_options
+                writer,
+                engine=engine,
+                storage_options=storage_options,
+                engine_kwargs=engine_kwargs,
             )
             need_save = True
 


Test patch:
diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py
index 9a8e4eff5470a..0560e12a00bf5 100644
--- a/pandas/tests/io/excel/test_writers.py
+++ b/pandas/tests/io/excel/test_writers.py
@@ -11,6 +11,7 @@
 import numpy as np
 import pytest
 
+from pandas.compat._constants import PY310
 import pandas.util._test_decorators as td
 
 import pandas as pd
@@ -1115,6 +1116,38 @@ def test_bytes_io(self, engine):
             reread_df = pd.read_excel(bio, index_col=0)
             tm.assert_frame_equal(df, reread_df)
 
+    def test_engine_kwargs(self, engine, path):
+        # GH#52368
+        df = DataFrame([{"A": 1, "B": 2}, {"A": 3, "B": 4}])
+
+        msgs = {
+            "odf": r"OpenDocumentSpreadsheet() got an unexpected keyword "
+            r"argument 'foo'",
+            "openpyxl": r"__init__() got an unexpected keyword argument 'foo'",
+            "xlsxwriter": r"__init__() got an unexpected keyword argument 'foo'",
+        }
+
+        if PY310:
+            msgs[
+                "openpyxl"
+            ] = "Workbook.__init__() got an unexpected keyword argument 'foo'"
+            msgs[
+                "xlsxwriter"
+            ] = "Workbook.__init__() got an unexpected keyword argument 'foo'"
+
+        # Handle change in error message for openpyxl (write and append mode)
+        if engine == "openpyxl" and not os.path.exists(path):
+            msgs[
+                "openpyxl"
+            ] = r"load_workbook() got an unexpected keyword argument 'foo'"
+
+        with pytest.raises(TypeError, match=re.escape(msgs[engine])):
+            df.to_excel(
+                path,
+                engine=engine,
+                engine_kwargs={"foo": "bar"},
+            )
+
     def test_write_lists_dict(self, path):
         # see gh-8188.
         df = DataFrame(